package uk.ac.manchester.tewpl.iceiface.client.explorer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import uk.ac.manchester.tewpl.iceiface.client.Functors.IterativeLoader;
import uk.ac.manchester.tewpl.iceiface.client.Functors.ParamVoidCallback;
import uk.ac.manchester.tewpl.iceiface.client.Functors.VoidCallback;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.AddAirportToJourneyEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.ChangeSelectedAirportEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.ExplorerEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.InitEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.ResetEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.nodes.Airport;
import uk.ac.manchester.tewpl.iceiface.client.explorer.nodes.Node;
import uk.ac.manchester.tewpl.iceiface.client.explorer.nodes.Terminal;
import uk.ac.manchester.tewpl.iceiface.client.explorer.types.Mutable;
import uk.ac.manchester.tewpl.iceiface.client.explorer.types.Uri;
import uk.ac.manchester.tewpl.iceiface.client.explorer.ui.MapPanel;
import uk.ac.manchester.tewpl.iceiface.client.explorer.ui.ResultsPanel;
import uk.ac.manchester.tewpl.iceiface.client.explorer.ui.SidePanel;
import uk.ac.manchester.tewpl.iceiface.client.explorer.ui.StatusPanel;
import uk.ac.manchester.tewpl.iceiface.client.explorer.ui.UI;
import uk.ac.manchester.tewpl.iceiface.client.model.KBClassModel;
import uk.ac.manchester.tewpl.iceiface.client.model.KBIndividualModel;
import uk.ac.manchester.tewpl.iceiface.client.model.KBObjectPropertyModel;
import uk.ac.manchester.tewpl.iceiface.client.model.KBWorkspaceModel;
import uk.ac.manchester.tewpl.iceiface.client.service.KBServiceAsync;

import com.extjs.gxt.ui.client.Registry;
import com.extjs.gxt.ui.client.Style.LayoutRegion;
import com.extjs.gxt.ui.client.Style.Scroll;
import com.extjs.gxt.ui.client.core.XTemplate;
import com.extjs.gxt.ui.client.data.BaseModelData;
import com.extjs.gxt.ui.client.event.ButtonEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.MessageBoxEvent;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.event.WindowEvent;
import com.extjs.gxt.ui.client.store.GroupingStore;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.util.Margins;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.widget.Composite;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.Dialog;
import com.extjs.gxt.ui.client.widget.Info;
import com.extjs.gxt.ui.client.widget.LayoutContainer;
import com.extjs.gxt.ui.client.widget.MessageBox;
import com.extjs.gxt.ui.client.widget.Text;
import com.extjs.gxt.ui.client.widget.Window;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.form.ComboBox;
import com.extjs.gxt.ui.client.widget.form.Radio;
import com.extjs.gxt.ui.client.widget.form.RadioGroup;
import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
import com.extjs.gxt.ui.client.widget.grid.Grid;
import com.extjs.gxt.ui.client.widget.grid.GridGroupRenderer;
import com.extjs.gxt.ui.client.widget.grid.GroupColumnData;
import com.extjs.gxt.ui.client.widget.grid.GroupingView;
import com.extjs.gxt.ui.client.widget.grid.RowExpander;
import com.extjs.gxt.ui.client.widget.layout.BorderLayout;
import com.extjs.gxt.ui.client.widget.layout.BorderLayoutData;
import com.extjs.gxt.ui.client.widget.layout.FitLayout;
import com.extjs.gxt.ui.client.widget.layout.MarginData;
import com.extjs.gxt.ui.client.widget.layout.RowData;
import com.extjs.gxt.ui.client.widget.toolbar.FillToolItem;
import com.extjs.gxt.ui.client.widget.toolbar.LabelToolItem;
import com.extjs.gxt.ui.client.widget.toolbar.ToolBar;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;

public class Explorer extends Composite {
	
	// Widget containers
	private LayoutContainer container;
	
	// Initialisation variables
	boolean initStarted, initialised;
	
	// UI Panels
	private UI mapPanel;
	private UI sidePanel;
	private UI resultsPanel;
	private UI statusPanel;
	private Collection<UI> panels;
	
	// Status panel control
	private boolean showStatusPanel = true;
	
	// KnowledgeBase variables
	private KBServiceAsync kbservice = Registry.get("kbservice");
	private KBWorkspaceModel ws = Registry.get("workspace");
	
	// Airport data and formatting
	private final Uri [] uris = DATA.uris;
	private final String [] airportLabels = DATA.airportLabels;
	private final Point [] airportOffsets = DATA.airportOffsets;
	private final Point [] airportLabelOffsets = DATA.airportLabelOffsets;
	
	// Airport variables
	private Collection<Airport> airports;
	private Mutable<Airport> airportSelected = new Mutable<Airport>();
	
	// Journey
	private LinkedList<Airport> journey = new LinkedList<Airport>();
	
	// Time period variables
	private ArrayList<KBIndividualModel> timePeriods;
	
	/**
	 * Create new Explorer instance
	 * 
	 * This will only create a new Explorer instance,
	 * and no data reteival will be started until
	 * init() is called.
	 */
	public Explorer() {
		// Initialise top container
		container = new LayoutContainer();
		container.setLayout(new BorderLayout());
		
		// Register
		Registry.register("explorer", this);
		
		// Return initialised container
		this.initComponent(container);
	}
	
	/**
	 * Initialise the widget, i.e. populate with data from KB
	 */
	public void init() {
		// Ensure init() is only called once
		if (!initStarted) {
			initStarted = true;
			container.mask("Loading...");
			setCursor("wait");
			
			// Load airports first
			airports = new ArrayList<Airport>();
			loadAirport(0, new VoidCallback() {
				public void work() {
					loadTimePeriods(); // When all airports loaded, load time periods
				}
			});
		}
	}
		
	/*
	 * Load airport i from the database
	 */
	private void loadAirport(final int i, final VoidCallback cb) {
		if (i>=0 && i<uris.length) { // Check there are still airports to create
			// Create i'th airport
			container.mask("Loading airport: " + (i+1) + "/" + uris.length + "...");
			new Airport(uris[i].toString(), airportLabels[i], new AsyncCallback<Airport>() {
	
				@Override
				public void onFailure(Throwable e) {
					MessageBox.alert("Error", "Unable to load airport!\n\n" + e.getMessage(), null);
				}
	
				@Override
				public void onSuccess(Airport airport) {
					airport.setLabelOffset(airportLabelOffsets[i]); // Store label offset data
					airport.setXY(airportOffsets[i]);
					airports.add(airport); // Add to airport arraylist
					loadAirport(i+1, cb); // Load next airport
				}
				
			}); // new Airport
		} else { // When all airports have been loaded
			cb.work(); // Call callback
		}
	}
	
	/*
	 * Load Time Periods
	 */
	private void loadTimePeriods() {
		container.mask("Loading Time Periods...");
		// Load time period class
		timePeriods = new ArrayList<KBIndividualModel>();
		kbservice.findClass("http://medicine.manchester.ac.uk/TEWPL/CRAMPONv6#Time_Period",
				ws, new AsyncCallback<KBClassModel>() {
					@Override
					public void onFailure(Throwable err) {
						MessageBox.alert("Error", "Unable to load Time Period class!\n\n" + err.getMessage(), null);
					}
					@Override
					public void onSuccess(KBClassModel cls) {
						// Load time period individuals
						kbservice.findIndividuals(cls, ws, new AsyncCallback<Collection<KBIndividualModel>>() {
							@Override
							public void onFailure(Throwable err) {
								MessageBox.alert("Error", "Unable to load Time Periods!\n\n" + err.getMessage(), null);
							}
							@Override
							public void onSuccess(Collection<KBIndividualModel> indivs) {
								// Add loaded individuals into collection
								timePeriods.addAll(indivs);
								// Initialise panels
								initPanels();
							}
							
						});
					}
		});
		
	}
	
	/*
	 * Once all data retrieved from DB, push relevant data
	 * to relevant UI panels
	 */
	private void initPanels() {		
		container.mask("Initialising UI...");
		
		// Register variables
		Registry.register("airports", airports);
		Registry.register("airportSelected", airportSelected);
		Registry.register("journey", journey);
		Registry.register("timePeriods", timePeriods);
		
		// Register utilities class
		Registry.register("utils", new Utils());
		
		// Create panels
		mapPanel = new MapPanel(this);
		sidePanel = new SidePanel(this);
		resultsPanel = new ResultsPanel(this);
		
		// Add panels to collection
		panels = new ArrayList<UI>();
		panels.add(mapPanel);
		panels.add(sidePanel);
		panels.add(resultsPanel);
		
		// Status panel
		if (showStatusPanel) {
			statusPanel = new StatusPanel();
			panels.add(statusPanel);
		}
		
		// Iteratively initialise panels
		final Iterator<UI> iter = panels.iterator();
		IterativeLoader<UI, VoidCallback> initPanels = new IterativeLoader<UI, VoidCallback>() {
			@Override
			public void load(UI panel, final VoidCallback callback) {
				try {
					panel.init(new VoidCallback() {
						@Override
						public void work() {
							if (iter.hasNext())
								load(iter.next(), callback);
							else
								callback.work();
						}
					});
				} catch (Exception err) {
					MessageBox.alert("Error", "Error initialising UI:\n" + err.toString(), null);
				}
			}
		};
		
		// Run IterativeLoader
		initPanels.load(iter.next(), new VoidCallback() {
			@Override
			public void work() {
				container.unmask();
				finishedInit();
			}
		});
	}
	
	/*
	 * Once all data init data has been pulled from
	 * the DB, build and display the interface
	 */
	private void finishedInit() {
		// Set up container layout
		container.setLayout(new BorderLayout());
		
		// North, contains map + filter panel
		LayoutContainer northPanel = new LayoutContainer();
		northPanel.setLayout(new BorderLayout());
		
		BorderLayoutData northPanelData = new BorderLayoutData(LayoutRegion.NORTH, 460);
		northPanelData.setMargins(new Margins(4,4,0,4));
		northPanelData.setSplit(true);
		
		// Map panel
		BorderLayoutData mapPanelData = new BorderLayoutData(LayoutRegion.CENTER);
		mapPanelData.setMargins(new Margins(0,4,0,0));
		
		// Side panel
		BorderLayoutData sidePanelData = new BorderLayoutData(LayoutRegion.EAST, 270);
		sidePanelData.setSplit(true);
		
		// Add north panels
		northPanel.add(mapPanel, mapPanelData);
		northPanel.add(sidePanel, sidePanelData);
		
		// Results panel
		BorderLayoutData resultsPanelData = new BorderLayoutData(LayoutRegion.CENTER);
		resultsPanelData.setMargins(new Margins(4));
		
		// Add panels
		container.add(northPanel, northPanelData);
		container.add(resultsPanel, resultsPanelData);
		
		// Status panel window
		if (showStatusPanel) {
			Window statusWindow = new Window();
			statusWindow.setSize(200,500);
			statusWindow.setLayout(new FitLayout());
			statusWindow.add(statusPanel);
			statusWindow.show();
			statusWindow.setPosition(this.getAbsoluteLeft() + this.getOffsetWidth() + 15,
					this.getAbsoluteTop());
		}
		
		// Update interface
		setCursor("default");
		container.unmask();
		container.layout();
		update(new InitEvent());
	}
	
	/**
	 * Sets the CSS cursor for the UI
	 * @param String cursor
	 */
	public void setCursor(String cursor) {
		setStyleAttribute("cursor", cursor);
	}
	
	/**
	 * Reset the interface
	 * i.e. remove all selections, clear results, etc
	 */
	public void reset() {		
		// Clear filters
		//filterCount = 0;
		//for (Airport airport : airports)
			//airport.clearFilteredResults();
		
		//initialFilterKeyword = null;
		//initialFilterAirport = null;
		
		// Clear airport linking details, filtering
		for (Airport airport : airports) {
			airport.setNextInJourney(null);
			airport.setPreviousInJourney(null);
			airport.clearFilters();
		}
		
		// Clear airport selection
		airportSelected.set(null);
		
		// Clear journey
		journey.clear();
		
		update(new ResetEvent());
	}
	
	/**
	 * Update all of the UI panels within the Explorer.
	 */
	public void update(ExplorerEvent event) {
		for (UI panel : panels)
			panel.handle(event);
	}
	
	/**
	 * Add a given airport to the selection list
	 * 
	 * @param airport Airport to add to route
	 */
	public void addAirportToJourney(Airport airport) {
		// Add airport to journey
		if (journey.size() > 0) {
			journey.getLast().setNextInJourney(airport);
			airport.setPreviousInJourney(journey.getLast());
		}
		
		journey.add(airport);
		
		// Ensure most recetly added airport is 'currently selected'
		airportSelected.set(airport);
		
		// Push changes to UI
		update(new AddAirportToJourneyEvent(airport));
	}
	
	/**
	 * Marks the given Airport as the currently selected airport on the route
	 */
	public void setSelectedAirport(Airport airport) {
		airportSelected.set(airport);
		update(new ChangeSelectedAirportEvent(airport));
	}

	/**
	 * Ask the user for confirmation to reset the interface
	 */
	public void confirmReset() {
		/*MessageBox.confirm("Reset map", "Would you like to reset the map?", new Listener<MessageBoxEvent>() {
			@Override
			public void handleEvent(MessageBoxEvent be) {
				 if (be.getButtonClicked().getItemId().equals(Dialog.YES))
					 reset();
			}
		});*/
		reset();
	}	

//	
//	/*
//	 * Add new filter
//	 */
//	private void addFilter(final Airport airport) {
//		MessageBox.prompt("Keyword Search", "Please enter a search term:", new Listener<MessageBoxEvent>() {
//			@Override
//			public void handleEvent(MessageBoxEvent ev) {
//				if (ev.getButtonClicked().getItemId().equals(Dialog.OK)) {
//					if (ev.getValue() != null) {
//						// Mask parent window
//
//						Info.display("", "Filter count = " + filterCount + ", filterExists == " + bb);
//						
//						String filter = ev.getValue();
//						
//						//if (filterCount == 0) { // If this is the first filter
//						if (bb) {
//							window.el().mask("Running search...");
//							window.setCursor("wait");
//							bb = false;
//							filterCount = 10;
//							// Perform search
//							airport.getResultsWithFilter(filter, new AsyncCallback<Collection<FilterResult>>() {
//								@Override
//								public void onFailure(Throwable err) { // Fail
//									MessageBox.alert("Keyword search", "Search failed with error:\n" + err.getMessage(), null);
//									// Unmask parent window
//									window.el().unmask();
//									window.setCursor("default");
//								}
//								@Override
//								public void onSuccess(Collection<FilterResult> filterResults) {
//									if (filterResults != null) { // If results
//										// Apply filter
//										applyFilter(filterResults);
//										
//										/*
//										 * Tell airports filter is applied;
//										 * used for count returns
//										 */
//										
//										for (Airport airport : airports)
//											airport.setFilterIsApplied();
//										
//										// Update interface
//										filterInterfaceUpdate(airport);
//										
//										// Message to user
//										/*MessageBox.info("Results", "Search completed with " + filterResults.size() +
//												" results at " + airport.getLabel(), null);*/
//									} else { // If not results
//										MessageBox.info("Results", "No results found!", null);
//									}
//									
//									// Unmask parent window
//									window.el().unmask();
//									window.setCursor("default");
//									
//									// Redraw map
//									//draw();
//								} // onSuccess
//							}); // getResultsWithFilter
//						} else { // If a filter has already been applied
//							/*
//							 * Run through airport + it's terminals, delete all FilterResults that don't match,
//							 * ensure related individuals (property values) are deleted from followed on
//							 * airports/terminals.
//							 */
//							
//							Collection<FilterResult> toRemove = new ArrayList<FilterResult>();
//							Collection<FilterResult> toRemove2 = new ArrayList<FilterResult>();
//							
//							if (airport.getFilterResults() != null) {
//								for (FilterResult filterResult : airport.getFilterResults()) {
//									String label = filterResult.getIndividual().getLabel();
//									String description = filterResult.getIndividual().getDescription();
//									
//									boolean labelMatch = label.matches("(?i).*" + filter + ".*");
//									boolean descMatch = description.matches("(?i).*" + filter + ".*");
//									
//									System.err.println("Seaching: " + label + " with : " + filter  + " == " + labelMatch);
//									
//									if (!labelMatch && !descMatch) { // If no matches
//										// Mark result to be removed
//										toRemove.add(filterResult);
//									}
//								}
//								
//								for (FilterResult filterResult : toRemove) {
//									airport.removeFilterResult(filterResult);
//								}
//								
//							}
//							
//							if (airport.getTerminals() != null) {
//								for (Terminal terminal : airport.getTerminals()) {
//									if (terminal.getFilterResults() != null) {
//										for (FilterResult filterResult : terminal.getFilterResults()) {
//											String label = filterResult.getIndividual().getLabel();
//											String description = filterResult.getIndividual().getDescription();
//											
//											boolean labelMatch = label.matches("(?i).*" + filter + ".*");
//											boolean descMatch = description.matches("(?i).*" + filter + ".*");
//											
//											System.err.println("Seaching: " + label + " with : " + filter  + " == " + labelMatch);
//											
//											if (!labelMatch && !descMatch) { // If no matches
//												// Mark result to be removed
//												toRemove2.add(filterResult);
//											}
//										}
//										
//										for (FilterResult filterResult : toRemove2) {
//											terminal.removeFilterResult(filterResult);
//										}
//										
//									}
//								}
//							}
//							
//							Collection<FilterResult> totalRemoved = new ArrayList<FilterResult>();
//							totalRemoved.addAll(toRemove);
//							totalRemoved.addAll(toRemove2);
//							
//							// Remove filter results
//							for (FilterResult filterResult : totalRemoved) {
//								// For each of the sets of related individuals
//								for (Collection<KBIndividualModel> propertySets : filterResult.getProperties().values()) {
//									// For each related Individual
//									for (KBIndividualModel property : propertySets) {
//										String uri = property.getClasses().iterator().next().getUri();
//										
//										// Find class for individual on map
//										Node node = getCachedNode(new Uri(uri));
//										if (node != null) {
//											FilterResult rem = null;
//											// Remove item from node's results
//											for (FilterResult result : node.getFilterResults()) {
//												if (result.getIndividual() == property) {
//													rem = result;
//													break;
//												}
//											}
//											
//											// Remove result from related node
//											if (rem != null) {
//												node.removeFilterResult(rem);
//											}
//										}
//											
//									}
//								}
//								
//							}
//								
//							airport.addFilterKeyword(filter);
//														
//							// Update interface
//							filterInterfaceUpdate(airport);
//							
//							// Unmask parent window
//							window.el().unmask();
//							window.setCursor("default");
//							
//							
//							
//						} // if (filterCount = 0), else
//						
//					} else {
//						MessageBox.alert("Keyword Search", "No text was entered!", null);
//					} // if (value == null), else
//				} // if clicked ok
//			} // handleEvent
//		}); // MessageBox.prompt
//	}
//	
//	private void filterInterfaceUpdate(Airport airport) {		
//		// Update interface
//		filterCount += 1;
//		
//		// Update the AirportPanels
//		for (AirportPanel panel : airportPanels)
//			panel.updatePanel();
//		
//		// Update the forward routes
//		updateForwardRoutes();
//		
//		// Show results for this airport in grid
//		populateDataGrid(airport);
//		
//		detailContainer.layout();
//		
//		draw();
//	}
//	
//	/*
//	 * Apply filter
//	 */
//	private void applyFilter(Collection<FilterResult> filterResults) {
//		for (FilterResult result : filterResults){ // For each FilterResult
//			// Add individual for this result to airport
//			Node node = getCachedNode(new Uri(result.getIndividual().getClasses().iterator().next().getUri()));
//			if (node != null)
//				node.addFilterResult(result);
//			for (KBObjectPropertyModel property : result.getProperties().keySet()) { // For each object property
//				for (KBIndividualModel propValue : result.getProperties().get(property)) { // For each property value
//					// Get property value's class
//					Uri relatedClassUri = new Uri(propValue.getClasses().iterator().next().getUri());
//					
//					// Get node on map for property value's class
//					Node propertyValueNode = getCachedNode(relatedClassUri);
//					
//					if (propertyValueNode != null) // If related class exists on map
//						propertyValueNode.addFilterResult(new FilterResult(propValue, null));
//					
//				} // for each property value
//			} // for each property
//		} // for each individual
//		
//	}
//	
//	/*
//	 * Returns the number of applied filters, used to determine which of
//	 * Node.getMemberCount() or Node.getFilterResultsCount() should be used
//	 */
//	public int getFilterCount() {
//		return filterCount;
//	}

}